package flexibilityDesign;

import ilog.concert.IloConversion;
import ilog.concert.IloLinearNumExpr;
import ilog.concert.IloNumVar;
import ilog.concert.IloNumVarType;
import ilog.cplex.IloCplex;
import ilog.cplex.IloCplex.DoubleParam;
import ilog.cplex.IloCplex.Parameter;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.List;
import java.util.Random;

import concurrent.Executer;
import support.AlgorithmSupport;
import util.Lang;
import util.NullOutputStream;

public class TestSingleCut {
	private int n;// demand number
	private int m;// resource number
	private int M;// scenario counts

	private double[][] demands;
	private double[] capacities;
	private double[][] c;// link cost
	private double[][] pi;// profit margin

	private int simulation_index = 0;

	public void setSimulationIndex(int index) {
		this.simulation_index = index;
	}

	private boolean parallelCompute = false;
	private boolean approximateMaster = false;
	private boolean warmStart = false;
	private boolean strengthenCuts = false;

	public TestSingleCut(int n, int m, int M) {
		this.n = n;
		this.m = m;
		this.M = M;
	}

	public void setEnhance(boolean paral, boolean approximate, boolean warmStart, boolean strengthCut) {
		this.parallelCompute = paral;
		this.approximateMaster = approximate;
		this.warmStart = warmStart;
		this.strengthenCuts = strengthCut;
	}

	private void initData() {
		this.capacities = Support.readCapacities(n, m);
		// double cap = BigDecimal.valueOf(n * 100)
		// .divide(BigDecimal.valueOf(m), 5, BigDecimal.ROUND_HALF_EVEN)
		// .doubleValue();
		// this.capacities = new double[m];
		// for (int j = 0; j < m; j++)
		// this.capacities[j] = cap;
		this.demands = Support.readDemands(M, n);
		this.c = Support.readLinkCost(n, m);
		this.pi = Support.readProfitMargin(n, m);
	}

	public void Lshape() {
		this.initData();
		double LB = Double.NEGATIVE_INFINITY;
		double UB = Double.MAX_VALUE;
		double gap = Double.MAX_VALUE;

		try {
			long start = System.currentTimeMillis();
			IloCplex master = new IloCplex();
			master.setOut(new NullOutputStream());
			IloNumVar[][] Y = new IloNumVar[n][m];
			int[][] YValue = new int[n][m];// Y value
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < m; j++) {
					Y[i][j] = master.boolVar();
					YValue[i][j] = 1;
				}
			}
			double[][] corePoint = null;
			IloNumVar theta = master.numVar(-1000000, 1000000);

			double thetaValue = Double.NEGATIVE_INFINITY;
			double masterObjValue = 0;

			IloLinearNumExpr obj = master.linearNumExpr();
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < m; j++) {
					obj.addTerm(c[i][j], Y[i][j]);
				}
			}
			obj.addTerm(1.0, theta);
			master.addMinimize(obj);

			// set how often to use these cut strategies
			master.setParam(IloCplex.IntParam.FracCuts, 2);
			master.setParam(IloCplex.IntParam.DisjCuts, 2);

			double currentOptUB = Double.MAX_VALUE;
			int[][] currentOptSol = Lang.clone(YValue);

			// // @enhance warm start: set initial upper bound and solution
			// WarmStartResult warmStart = new WarmStartResult();
			// warmStart.runWarmStart();
			// currentOptUB = warmStart.upperBound;
			// currentOptSol = Lang.clone(warmStart.initSolution);
			// // pending enhance

			long timeLimite = AlgorithmSupport.TERMINATE_TIME_LIMITE;

			int iter = 1;
			while (true) {
				double estimatedObj = 0.0;
				if (iter > 1) {
					long masterStart = System.currentTimeMillis();

					if (this.approximateMaster) {
						// @enhance approximate master solve
						if (gap > 0.05)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.08);
						else if (gap > 0.03)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.05);
						else if (gap > 0.02)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.03);
						else if (gap > 0.01)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.02);
						else if (gap > 0.008)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.008);
						else if (gap > 0.007)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.007);
						else if (gap > 0.005)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.005);
						else if (gap > 0.003)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.003);
						else if (gap > 0.002)
							master.setParam(IloCplex.DoubleParam.EpGap, 0.002);
						else
							master.setParam(IloCplex.DoubleParam.EpGap, 0.001);
						// end enhance
					}

					// @enhance warm start: set initial upper bound and solution
					// master.setParam(DoubleParam.CutUp, currentOptUB);
					// master.setParam(DoubleParam.CutLo, LB);
					if (this.warmStart) {
						for (int i = 0; i < n; i++) {
							double[] initY = new double[m];
							for (int j = 0; j < m; j++)
								initY[j] = currentOptSol[i][j];
							master.addMIPStart(Y[i], initY);
						}
						// end enhance
					}

					master.setParam(IloCplex.DoubleParam.TiLim, timeLimite);

					if (master.solve()) {
						long masterEnd = System.currentTimeMillis();
						System.out.println("master time = " + (masterEnd - masterStart));
						for (int i = 0; i < n; i++)
							for (int j = 0; j < m; j++)
								YValue[i][j] = (int) master.getValue(Y[i][j]);
						masterObjValue = master.getObjValue();
						if (masterObjValue > LB)
							LB = masterObjValue;
						thetaValue = master.getValue(theta);

						for (int i = 0; i < n; i++) {
							for (int j = 0; j < m; j++) {
								if (YValue[i][j] == 1)
									estimatedObj += c[i][j];
							}
						}
					}

					// @enhance strengthening cuts
					if (this.strengthenCuts) {
						corePoint = new double[n][m];
						IloConversion[][] conversions = new IloConversion[n][m];
						for (int i = 0; i < n; i++) {
							for (int j = 0; j < m; j++) {
								conversions[i][j] = master.conversion(Y[i][j], IloNumVarType.Float);
								master.add(conversions[i][j]);
							}
						}
						if (master.solve()) {
							for (int i = 0; i < n; i++)
								corePoint[i] = master.getValues(Y[i]);
						}
						for (int i = 0; i < n; i++) {
							for (int j = 0; j < m; j++) {
								master.remove(conversions[i][j]);
							}
						}
					}
				}

				BigDecimal subProblemObjValue = BigDecimal.ZERO;
				BigDecimal et = BigDecimal.ZERO;
				BigDecimal[][] Et = new BigDecimal[n][m];
				for (int i = 0; i < n; i++)
					for (int j = 0; j < m; j++)
						Et[i][j] = BigDecimal.ZERO;

				if (this.parallelCompute) {
					// @enhance parallel computing
					Executer executer = new Executer(10, true);
					for (int k = 0; k < M; k++) {
						// @enhance strengthen cut
						if (this.strengthenCuts)
							executer.fork(new DualSolver(n, m, demands[k], YValue, corePoint, pi, capacities, k));
						else
							executer.fork(new DualSolver(n, m, demands[k], YValue, pi, capacities, k));
					}
					List<DualResult> results = executer.join();
					executer.shutdown();
					for (DualResult dr : results) {
						if (dr != null) {
							subProblemObjValue = subProblemObjValue.add(BigDecimal.valueOf(dr.getObjValue()));
							et = et.add(dr.getEtm());
							for (int i = 0; i < n; i++)
								for (int j = 0; j < m; j++)
									Et[i][j] = Et[i][j].add(dr.getEt()[i][j]);
						}
					}
					// enhance parallel computing end
				} else {
					for (int sm = 0; sm < M; sm++) {
						IloCplex dual = new IloCplex();
						dual.setOut(new NullOutputStream());
						IloNumVar[] phi = dual.numVarArray(m, Double.NEGATIVE_INFINITY, 0.0);
						IloNumVar[] sigma = dual.numVarArray(n, Double.NEGATIVE_INFINITY, 0.0);
						IloNumVar[][] gamma = new IloNumVar[n][m];
						for (int i = 0; i < n; i++)
							for (int j = 0; j < m; j++)
								gamma[i][j] = dual.numVar(Double.NEGATIVE_INFINITY, 0.0);
						IloLinearNumExpr dobj = dual.linearNumExpr();
						for (int j = 0; j < m; j++)
							dobj.addTerm(capacities[j], phi[j]);
						for (int i = 0; i < n; i++)
							dobj.addTerm(demands[sm][i], sigma[i]);
						for (int i = 0; i < n; i++) {
							for (int j = 0; j < m; j++) {
								dobj.addTerm(YValue[i][j] * capacities[j], gamma[i][j]);

								// constraint
								dual.addLe(dual.sum(phi[j], sigma[i], gamma[i][j]), -pi[i][j]);
							}
						}
						dual.addMaximize(dobj);

						if (dual.solve()) {
							subProblemObjValue = subProblemObjValue.add(BigDecimal.valueOf(dual.getObjValue()));
							for (int j = 0; j < m; j++)
								et = et.add(BigDecimal.valueOf(dual.getValue(phi[j]) * capacities[j]));
							for (int i = 0; i < n; i++)
								et = et.add(BigDecimal.valueOf(dual.getValue(sigma[i]) * demands[sm][i]));
							for (int i = 0; i < n; i++) {
								for (int j = 0; j < m; j++) {
									Et[i][j] = Et[i][j].add(BigDecimal.valueOf(dual.getValue(gamma[i][j])
											* (-capacities[j])));
								}
							}
						}
						dual.end();
					}
				}

				estimatedObj += subProblemObjValue.divide(BigDecimal.valueOf(M), 5, BigDecimal.ROUND_HALF_EVEN)
						.doubleValue();
				if (estimatedObj < currentOptUB) {
					currentOptUB = estimatedObj;
					currentOptSol = Lang.clone(YValue);
				}

				et = et.divide(BigDecimal.valueOf(M), 5, BigDecimal.ROUND_HALF_EVEN);
				for (int j = 0; j < m; j++) {
					for (int i = 0; i < n; i++) {
						Et[i][j] = Et[i][j].divide(BigDecimal.valueOf(M), 5, BigDecimal.ROUND_HALF_EVEN);
					}
				}
				BigDecimal Etx = BigDecimal.ZERO;
				for (int i = 0; i < n; i++) {
					for (int j = 0; j < m; j++) {
						Etx = Etx.add(Et[i][j].multiply(BigDecimal.valueOf(YValue[i][j])));
					}
				}
				double wv = et.subtract(Etx).doubleValue();

				if (iter > 1) {
					gap = BigDecimal.valueOf(wv - thetaValue)
							.divide(BigDecimal.valueOf(-thetaValue), 5, BigDecimal.ROUND_HALF_EVEN).doubleValue();
				}
				System.out.println("@" + iter + ": " + gap);

				long timeTT = System.currentTimeMillis() - start;
				timeLimite = AlgorithmSupport.TERMINATE_TIME_LIMITE - timeTT;
				if (gap < 0.001 || timeTT > AlgorithmSupport.TERMINATE_TIME_LIMITE) {
					for (int i = 0; i < n; i++) {
						for (int j = 0; j < m; j++) {
							if (YValue[i][j] == 1)
								System.out.print("(" + i + "," + j + ") \t");
						}
						System.out.println();
					}
					long end = System.currentTimeMillis();
					double time = end - start;
					System.out.println("time consumed: " + time);
					System.out.println("masterObj= " + masterObjValue);

					StringBuilder builder = new StringBuilder("singlecut n/m=" + n + "/" + m + ";index="
							+ simulation_index + "\r\n");
					builder.append("ub=" + UB + ";lb=" + LB + "\r\n");
					builder.append("time=" + time + "; gap=" + gap + "\r\n");

					try {
						Writer writer = new BufferedWriter(new FileWriter(Lang.projectRoot()
								+ "\\data\\flexibilityDesign\\result\\result.txt", true));
						writer.write(builder.toString());
						writer.close();
					} catch (IOException e) {
						e.printStackTrace();
					}

					break;
				} else {
					iter++;
					IloLinearNumExpr optimalCut = master.linearNumExpr();
					for (int i = 0; i < n; i++)
						for (int j = 0; j < m; j++)
							optimalCut.addTerm(Et[i][j].doubleValue(), Y[i][j]);

					master.addGe(master.sum(optimalCut, theta), et.doubleValue());
				}
			}
			master.end();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	class WarmStartResult {
		private int warmCount = 5;// warm run times
		private int warmSize = 10;// instances for each warm run
		private int[][] randomIndexs = new int[warmCount][warmSize];

		double upperBound = Double.MAX_VALUE;
		int[][] initSolution = new int[n][m];

		private void generateRandomIndex() {
			int max = demands.length;
			Random random = new Random();
			for (int i = 0; i < warmCount; i++) {
				for (int j = 0; j < warmSize; j++) {
					randomIndexs[i][j] = random.nextInt(max);
				}
			}
		}

		void runWarmStart() {
			System.out.println("@ begin warm start");
			generateRandomIndex();

			double pro = BigDecimal.ONE.divide(BigDecimal.valueOf(warmSize), 5, BigDecimal.ROUND_HALF_EVEN)
					.doubleValue();

			for (int wi = 0; wi < warmCount; wi++) {
				try {
					IloCplex master = new IloCplex();
					master.setOut(new NullOutputStream());
					IloNumVar[][] Y = new IloNumVar[n][m];
					for (int i = 0; i < n; i++)
						for (int j = 0; j < m; j++)
							Y[i][j] = master.boolVar();

					IloNumVar[][][] x = new IloNumVar[warmSize][n][m];
					for (int sm = 0; sm < warmSize; sm++) {
						for (int i = 0; i < n; i++) {
							for (int j = 0; j < m; j++) {
								x[sm][i][j] = master.numVar(0.0, Double.MAX_VALUE);
							}
						}
					}
					IloLinearNumExpr obj = master.linearNumExpr();
					for (int i = 0; i < n; i++)
						for (int j = 0; j < m; j++)
							obj.addTerm(c[i][j], Y[i][j]);
					for (int sm = 0; sm < warmSize; sm++)
						for (int i = 0; i < n; i++)
							for (int j = 0; j < m; j++)
								obj.addTerm(-pi[i][j] * pro, x[sm][i][j]);
					master.addMinimize(obj);

					for (int sm = 0; sm < warmSize; sm++) {
						for (int j = 0; j < m; j++) {
							IloLinearNumExpr ca = master.linearNumExpr();
							for (int i = 0; i < n; i++) {
								ca.addTerm(1.0, x[sm][i][j]);
							}
							master.addLe(ca, capacities[j]);
						}
						for (int i = 0; i < n; i++) {
							IloLinearNumExpr de = master.linearNumExpr();
							for (int j = 0; j < m; j++) {
								de.addTerm(1.0, x[sm][i][j]);
							}
							master.addLe(de, demands[randomIndexs[wi][sm]][i]);
						}
						for (int i = 0; i < n; i++)
							for (int j = 0; j < m; j++) {
								IloLinearNumExpr cab = master.linearNumExpr();
								cab.addTerm(Y[i][j], capacities[j]);
								master.addLe(x[sm][i][j], cab);
							}
					}

					double[][] init = new double[n][m];
					for (int i = 0; i < n; i++)
						for (int j = 0; j < m; j++)
							init[i][j] = 1.0;
					for (int i = 0; i < n; i++)
						master.addMIPStart(Y[i], init[i]);

					if (master.solve()) {
						double objV = master.getObjValue();
						if (objV < upperBound) {
							upperBound = objV;
							for (int i = 0; i < n; i++) {
								for (int j = 0; j < m; j++) {
									initSolution[i][j] = (int) master.getValue(Y[i][j]);
								}
							}
						}

					}
					master.end();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			System.out.println("@ end warm start");
		}
	}

}
